今日内容
1.完善我们的account案例
2.分析案例中的问题
3.回顾之前讲过的一个技术:动态代理
4.动态代理另一种实现方式
5.解决案例中的问题
6.AOP的概念
7.spring中的AOP相关术语
8.spring中基于XML和注解的AOP配置
1. 问题引出
1.1 转账问题:
1.请看下面的示例:
*我在业务层中加入一个方法:模拟转账
```
public void transfer(String sourceName, String targetName, float money) {
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
```
2.执行之后发现:
由于有异常,转账失败。
但是转出账户的钱减少了,转入账户的钱却没有增加。
3. 分析:
1.事务管理的问题
首先应该确定,我们的代码中是有事务提交的,因为其中的增删改方法都能够正常执行且没有回滚的。
如果没有事务,那么增删改显然是无法提交的。
换句话说,不是没有事务造成的而是事务的管理出现了问题。
2.分析 QueryRunner对象
* 每次用QueryRunner时,都会再次创建一个新的对象,同时也会从数据池中拿出一个新的连接。
* 分析模拟转账的代码和数据库交互了几次?
发现整个过程,模拟转账代码和数据库交互了4次。那么这样就会有4个connection对象。
而每个connection也都有自己的独立事务,那么情况就是:
第一个connection成功执行了---事务提交
第二个connection成功执行了---事务提交
第三个connection成功执行了---事务提交
第四个connection没成功----------事务不提交
4.问题就是:
事务被自动控制了,即我们使用了connection对象的setAutoCommit(true)方式控制事务。
此方式控制事务,如果我们每次执行一条sql语句没有问题。
但是如果业务方法一次需要执行多条sql语句,这种方式就无法实现功能了。
也可以这样理解:我们应该让事务的控制在业务层,但是之前事务的控制都在持久层。
5. 解决思路:让同一个connection来控制整个转账过程。
我们需要使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程中只有
一个能控制事务的对象。
当前线程:一个测试方法(在该例中,是一个Service方法执行完整的过程)走完的全过程。
1.2 解决转账问题。
*思路:用ThreadLocal来管理事务:
就是始终保持当前线程(测试完一个方法,走完了就是这一个线程)只有一个在使用中
的Connection,而且只要有需要,我们总有办法拿到与当前线程绑定的唯一的Connection。
让每个dao里面的方法取得的Connection和service方法里面用到的Connection保持统一
,而且每个线程中也只有这一个Connection。
靠ThreadLocal来保证,靠ThreadLocal中的get()方法来获取当前线程中唯一的Connection
对象。
只要是靠ThreadLocal获取的Connection对象,那就是我们用来控制事务的唯一一个Connection
1.2.1 先写一个连接工具类:ConnectionUtils
分析一下相关代码:
1.ThreadLocal对象,用泛型指定管理对象是java.sql.Connection
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
2.数据源成员变量:
private DataSource dataSource;
注意:dataSource在bean.xml是单例创建的,所以使用时,也用的是同一个dataSource。
在这里注入了一个dataSource:
@Autowired
private DataSource dataSource;
当一个线程走完时(Service中的方法),就会解除和coon的绑定,那么下次在从ThreadLo
cal获取coon时,就会从同一个dataSource中获取连接coon。
3.从当前线程中获取一个连接:
```
//1.先从ThreadLocal上获取
Connection coon = tl.get();
//2.判断当前线程上是否有连接
if(coon == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
coon = dataSource.getConnection();
//4.存入ThreadLocal
tl.set(coon);
}
//5.返回当前线程上的连接
return coon;
```
4.把连接和线程解绑:方法
tl.remove();
```
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
@Autowired
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* 控制事务,是靠connection把手动提交改成自动提交
* 再通过commit和rollback来控制事务。
*/
public Connection getThreadConnection(){
try{
//1.先从ThreadLocal上获取
Connection coon = tl.get();
//2.判断当前线程上是否有连接
if(coon == null){
//3.从数据源中获取一个连接,并且存入ThreadLocal中
coon = dataSource.getConnection();
//4.存入ThreadLocal
tl.set(coon);
}
//5.返回当前线程上的连接
return coon;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
```
1.2.2 再写一个事务管理的工具类 TransactionManager
分析一下相关代码:
1. 连接工具类ConnectionUtils 属性,用Spring注入
@Autowired
private ConnectionUtils connectionUtils;
2. Connection对象开启事务
connectionUtils.getThreadConnection().setAutoCommit(false);
3.提交事务
4.回滚事务
5.关闭连接并与ThreadLocal解绑
connectionUtils.getThreadConnection().close();
connectionUtils.removeConnection();
注意1:为什么要解绑?
我们从连接池中获取的连接connection在关闭后.close(),并不是真正的关闭,
而是把它还回连接池中,但是这个连接已经不能用了。
由于我们的线程适合connection绑定的,所以当我们下次再从这个当前唯一线程
中获取connection时,就还会得到这个失效的connection,所以我们在关闭connection
后,要将当前线程和connection解绑。
注意2:新的疑惑
解绑后不就又要绑定新的connection吗?不就又有新的控制事务的对象了吗?
首先我们要知道什么时候后解除绑定,是在AccountServiceImpl中的一个方法
执行完之后才会解绑,不是执行完一个SQL就会解绑。
执行完一个Service中的方法之后,这个线程,线程中的事务也就处理完了。
```
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
//等待Spring将其注入
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
//开启事务
public void beginTransaction(){
try{
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (Exception e){
e.printStackTrace();
}
}
//提交事务
public void commit(){
try{
connectionUtils.getThreadConnection().commit();
} catch (Exception e){
e.printStackTrace();
}
}
//回滚事务
public void rollback(){
try{
connectionUtils.getThreadConnection().rollback();
} catch (Exception e){
e.printStackTrace();
}
}
//释放连接
public void release(){
try{
connectionUtils.getThreadConnection().close();
//解绑
connectionUtils.removeConnection();
} catch (Exception e){
e.printStackTrace();
}
}
}
```
1.2.3 再写一个dao实现类
```
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtils.getThreadConnection(), "select * from account", new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Account findAccountById(Integer accountId) {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account where id = ?", new BeanHandler<Account>(Account.class), accountId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
```
分析:
runner.query(connectionUtils.getThreadConnection(), "select * from account", new
BeanListHandler<Account>(Account.class));
在这里每次传入的coon,都是从绑定了coon的线程中ThreadLocal变量获取的。那么只要ThreadLocal
不解绑,无论一个线程(Service的一个方法完整过程)中执行了几个SQL,都是同一个coon。
那样就控制只有事务的coon也只有一个。
1.2.4 再写一个Service实现类
```
public void transfer(String sourceName, String targetName, float money) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
} catch (Exception e){
//4.回滚操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.释放连接
txManager.release();
}
}
```
1.2.5 bean.xml
注意一个细节:
1.queryRunner是多例创建,里面有dataSource属性。
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
这里我们没有给queryRunner注入dataSource属性,如果我们在配置queryRunner的时候又给其注入
dataSource属性,那么每次创建queryRunner是,虽然queryRunner是多例创建的,但是注入的也是
同一个dataSource。因为dataSource是单例的,queryRunner的dataSource属性就回去容器中匹配
存在的dataSource,那么就会正好匹配的同一个。
2.dataSource是用的默认单例创建
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
所以每次传入的coon对象都是从同一个dataSource中获取的
runner.query(connectionUtils.getThreadConnection(), "select * from account", new
BeanListHandler<Account>(Account.class));
3. ConnectionUtils是TransactionManager的属性
ConnectionUtils,QueryRunner都是AccountDaoImpl的属性
我们配置的时候:
```
<!--配置Dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!--通过set方法:注入QueryRunner-->
<property name="runner" ref="queryRunner"></property>
<!--注入ConnectionUtils:因为ConnectionUtils也是TransactionManager的属性,
所以在TransactionManager注入了,这里就不在注入-->
</bean>
<!--配置事务管理类:TransactionManager-->
<bean id="transactionManager" class="com.itheima.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
```
我已经配置事务管理类:TransactionManager时,注入了ConnectionUtils属性,并且
了ConnectionUtils也是AccountDaoImpl的属性,那么此时在配置Dao对象时就可以不用
再注入ConnectionUtils属性了。
1.2.6
* 最后事务控制成功。
* 回归正常后,我们事务的控制又由持久层回到了业务层。
但是我们也发现配置变得非常的麻烦,非常繁杂的依赖注入,和及其臃肿的代码。
* 依赖有类之间的依赖,调用关系。也有方法之间的依赖,当一个方法在其他地方被多次调用时,
那么如果这个方法修改了方法名,就会产生巨大的工作量。
2 动态代理
2.1 动态代理---基于接口的动态代理
背景:
作为一个代理商,他有一个选择生产厂家的准则:必须有销售和售后
在java何为标准呢? :接口。
明确一点:代理的是IProducer还是Producer
接口是Producer实现的标准,即Producer中的方法。我们增强的Producer的方法,
所以代理的是Producer对象。
只是得到的代理对象,我们用IProducer接收。
需求:消费者支付的货款,厂家得到80%,增强生产者的销售方法。
1. 对生产厂家要求的接口
```
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterServicce(float money);
}
```
2. 生产者
```
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterServicce(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
```
3. 模拟一个消费者
```
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//注意一下:参数是money:float,这里money*0.8之后就不是float了,所以要money*0.8f
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
producer.saleProduct(10000f);
proxyProducer.saleProduct(10000f);
}
}
```
4. 分析一下基于接口的动态代理:被代理类最少实现一个接口,如果没有则不能使用
特点:字节码随用随创建,随用随加载。
作用:不修改源码的基础上,对方法增强
涉及的类:Proxy
提供者:JDK官方
如何创建代理对象:
使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader : 类加载器
它是用于加载代理对象字节码的,写的是被代理对象的类加载器。
或者说和被代理对象使用相同的类加载器,固定写法。
* Class[] :字节码数组
它是用于让代理对象和被代理对象有相同的接口。只要两个都实现同一个接口,
两个就都会有这个接口中的方法。
写法:代理谁就写谁的接口---固定写法。
* InvocationHandler:用于提供增强的代码
它是让我们写如何代理,我们一般都是写一个该接口的实现类。
通常情况下都是匿名内部类,但不是必须的。
此接口的实现类都是谁用谁写。
5. 分析一下实例化代理对象的方法:
```
Producer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//注意一下:参数是money:float,这里money*0.8之后就不是float了,所以要money*0.8f
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
```
这就是实例化一个代理对象的方法:
1.参数一:被代理对象Produce的类加载器
producer.getClass().getClassLoader()
2.参数二:被代理对象实现的所有接口
producer.getClass().getInterfaces(),
3.参数三:InvocationHandler该接口的实现类。
```
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
```
这里用匿名内部类的方式实现的:里面重写了invoke()方法。
new InvocationHandler(){
public Object invoke(){
//增强的方法
}
}
4. InvocationHandler接口中的invoke方法()
注意:当代理对象执行被代理对象的任何接口和方法都会经过该方法。
匿名内部类访问外部成员变量时,外部成员要求时最终的final修饰。
参数1:proxy 代理对象的引用
参数2:method 当前执行的方法
参数3:args 当前方法所需的参数
返回值 和被代理对象有相同的返回值
* 注意:如果被代理对象的返回值是void,那么invoke放回的就是null。
```
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
````
5.用代理对象调用一下被代理对象的方法
proxyProducer.saleProduct(10000f);
2.2 动态代理---基于子类的动态代理
需求:消费者支付的货款,厂家得到80%,增强生产者的销售方法。
生产者Producer没有继承任何接口。
2.2.1 概述:基于子类的动态代理
基于子类的动态代理:
涉及的类:Enhancer
提供者:第三方cglib库
如何创建代理对象:
使用Enhancer类中的create方法
创建代理对象的要求:
被代理类不能是最终类,如果是最终类,就不能在创建子类了,也就没法创建代理对象了
2.2.2 生产者Producer
```
public class Producer{
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
public void afterServicce(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
```
注意:它是一个普通类,没有实现接口
2.2.3 模拟一个消费者
```
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
```
分析一下Enhancer类中的create方法:
参数1:Class 字节码
它是用于指定被代理对象的字节码,想代理谁就写谁的.getClass()
参数2:Callback 用于提供增强的代码
它是让我们写如何代理,我们一般都是写一个该接口的实现类,MethodInterceptor
(public interface MethodInterceptor extends Callback)
分析一下:Callback的实现类MethodInterceptor
```
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
//注意一下:参数是money:float,这里money*0.8之后就不是float了,所以要money*0.8f
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
```
其中重写了intercept方法,代理对象执行的所有被代理对象的方法都会在此处被拦截。
参数1: proxy 代理对象的引用
参数2: method 当前执行的方法
参数3: args 当前方法所需的参数
参数4: methodProxy 当前执行方法的代理对象,一般用不到
返回值: 和被代理对象方法中的放回值是一样的
* 用生产者来接收代理对象,最后用代理对象调用一下被代理对象的方法
proxyProducer.saleProduct(10000f);
2.3 动态代理的分析
分类:
基于接口的动态代理
生成的代理对象和被代理对象继承的是同一个接口,所以可以用接口接收这个代理对象
基于子类的动态代理
生成的代理对象继承了被代理对象。所以被代理对象不能被final修饰。
3. account案例的最终改进版:让事务控制和业务层的方法分离
主要改进:将AccountServiceImpl中的重复代码抽取出来。原先的AccountServiceImpl中的每个
业务方法都有事务控制的一系列操作,所以将之抽取出来。
思路: 调用AccountServiceImpl中的业务方法时,我们用动态代理方式创建一个AccountServiceImpl
的代理对象proxyAccountService,它增强了业务方法,为每个业务方法都添加了事务管理。
1.AccountServiceImpl
```
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao){
this.accountDao = accountDao;
}
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
public void transfer(String sourceName, String targetName, float money) {
System.out.println("transfer...");
//1.根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney() - money);
//4.转入账户加钱
target.setMoney(target.getMoney() + money);
//5.更新转出账户
accountDao.updateAccount(source);
//int i = 1/0; //模拟转账异常
//6.更新转入账户
accountDao.updateAccount(target);
}
}
```
2. 创建AccountServiceImpl的代理对象的工厂: BeanFactory
```
public class BeanFactory {
private IAccountService accountService;
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public IAccountService getAccountService(){
IAccountService proxyAccountService =
(IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务的支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
});
return proxyAccountService;
}
}
```
3 在配置一下bean.xml
其他没变同#### 1.2
3. AOP
3.1 AOP的概述
1.什么是AOP
AOP:Aspect Oriented Programming 面向切面编程
也是降低耦合度的一种技术,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑
各部分之间的耦合度降低,提高程度的可重用性,同时提高开发的效率。
简单的说,它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,
在不修改源码的基础上,对我们已有的方法进行增强。
2.AOP的作用及优势
作用
在程序运行期间,不修改代码对已有方法进行增强。
优势
减少重复代码
提高开发效率
维护方便
3.AOP的实现方式
使用动态代理技术。
4 Spring中的AOP
4.1 相关术语和AOP概念
1. 关于代理的选择:
在spring中,框架会根据目标是否实现了接口来决定采用哪种动态代理的方式。
我们学习spring的AOP,就是通过配置的方式,实现上一部分动态代理抽取事务的功能
Spring选择AOP是有一个准则的,根据是否实现了接口
是:基于接口的动态代理
要求被代理的对象最少实现一个接口(得到的代理对象要和被代理对象实现相同的接口)
否:基于子类的动态代理
要求被代理类不能是最终类(因为得到的代理对象要继承被代理对象)
基于子类的动态代理也可以用于基于接口
2. AOP的相关术语
1.Joinpoint(连接点):
所谓连接点是指那些被拦截到的点,在spring中,这些点指的是方法,因为
spring只支持方法类型的连接点。
举个例子:
我们怎么把增强的代码(事务控制的代码)加到业务中来呢?
Service层中的方法:这些方法可以加上事务的支持,从而让我们的这些业务方法
形成一个完整的业务逻辑。
此时Service层中的方法就可以被认为是连接点
2.Pointout(切入点)
所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。
我们可以指定Service层哪些方法被增强,哪些方法不被增强。
```
if("test".equals(method.getName())){
// 如果是test函数:按照真实对象中的方法执行:没有事务支持
// 调用method对象的invoke方法,并且指明该method是accountService中的method,且传递的参数args也不改变
return method.invoke(accountService, args);
}
```
这里:
如果是test函数:按照真实对象中的方法执行:没有事务支持
调用method对象的invoke方法,并且指明该method是accountService中的method,
且传递的参数args也不改变。
所以通俗的来说,切入点就是那些被增强的方法。
在此例中,test方法是可以被增强的但是没有增强,所以是连接点,但不是切入点。
所以所有的切入点都是连接点,反之不然。
3. Advice(通知/增强)
所谓通知是指拦截到的Joinpoint之后要做的事情就是通知。
通知的类型:
前置通知
后置通知
异常通知
最终通知
环绕通知
4.Introduction(引介)
引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为
类动态地添加一些方法或Field
5.Target(目标对象)
代理的目标对象:被代理对象
6.Weaving(织入)
是指把增强应用到目标对象来创建新的代理对象的过程。
即:
我们原先的service没法实现事务的支持,于是我用了动态代理的技返回了一个代理对象。
在返回这个代理对象中,我们在其中加入的事务。这样一个加入事务的过程,就叫做织入。
7. Proxy(代理)
一个类被AOP织入增强后,就产生的一个结果代理类。
8.Aspect(切面):
是切入点和通知(引介)的结合。
例如:
被增强的方法:transfer()
通知:就是那些提供了公共代码的类。(为每个service中的方法提供事务支持的类。)
那这些公共代码什么时候执行呢?
我们写代码的时候很明确:那么配置的时候怎么说明白呢?
我们开启事务在执行SQL之前
提交事务在执行SQL之后
那什么时候能把这些也说明白呢?
建立切入点方法和通知方法在执行调用的对应关系:就是切面。
我们写代码的时候很明确:那么配置的时候怎么说明白呢?
切面:建立切入点方法和通知方法在执行调用的对应关系:就是切面。
3. 学习Spring中的AOP我们要明确的事
我们要明确:
1.我们在学习spring-AOP时,我们要做哪些(开发阶段)?
2.框架能为我们做哪些(运行阶段)?
4.2 Spring基于XML的AOP配置步骤
0.pom.xml
* 要用AOP的配置,所以要在bean.xml中将AOP的约束放进来。
ctrl+F : 搜索xmlns:aop
* 导入aspectjweaver依赖
aspectj是一个软件联盟:能解析出我们在bean.xml中配置的切入点表达式。
1.把通知类(Logger)bean也交给spring来管理
2.使用aop:config标签表明开始AOP的配置
3.使用aop:aspect标签开始配置切面
id属性:给切面提供一个唯一标识
随便取:logAdvice()表明是,log通知
ref属性:使用指定通知类(Logger)的bean的id。
4.在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在的示例是让通知类(Logger)中的printLog方法在切入点之前执行,所以是前置通知。
aop:before:表示配置前置通知
method属性:用于指定Logger类中的哪个方法是前置通知
5.pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强,
切入点表达式的写法:
关键字:execution(表达式)
表达式: 访问修饰符 返回值 包名.包名.包名... 类名.方法名(参数列表)
标准的表达式写法 public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
* 分析一下相关的配置:
1. 首先有一个service,这个service有方法(切入点方法)需要增强---在方法执行前执行printLog()方法
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
2. 我们在配置一个通知类(Logger),他是提供共用代码(增强代码)
<bean id="logger" class="com.itheima.utils.Logger"></bean>
3. 又配置了切面(建立切入点方法和通知方法在执行调用的对应顺序关系)
4.通知类(Logger)中有一个方法printLog(),它是在增强方法前面执行
里面的切入点表达式明确的说:指定切入点表达式,即对哪个方法增强。
<aop:before method="printLog" pointcut="execution(public void
com.itheima.service.impl.AccountServiceImpl.saveAccount())">
</aop:before>
具体怎么增强:创建代理对象实现。
5.注意的一些事项:现在的切入点表达式这样写是有些繁琐的
<aop:before method="printLog" pointcut="execution(public void
com.itheima.service.impl.AccountServiceImpl.saveAccount())">
</aop:before>
这只是为service层中的一个方法配置了切面,但是一般我们要为service的每个方法都配置切面,
如果每个service方法都这样配置切面,那么显然要写太多的配置。
4.3 接下来我们来说一下 : 切入表达式的一些简单常用写法
简单的写法:全通配写法
* *..*.*(..)
*我们来分析一下怎么得到的:
1.访问修饰符可以省略 public
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
2.返回值可以使用通配符,代表任何返回值 :
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
3.包名可以使用通配符,表示任意包,但是有几级包,就需要写几个*:
* *.*.*.*.AccountServiceImpl.saveAccount()
4.包名可以使用..表示当前包及子包:
* *..AccountServiceImpl.saveAccount()
代表了只要任意包下的只要有一个AccountServiceImpl.saveAccount()就是我们要找的,会被增强
5.类名和方法名,都可以使用*来实现通配:
* *..*.saveAccount() :只有saveAccount()配置了增强(配置了切面)
* *..*.*() : 有两个方法deleteAccount()和saveAccount()配置了增强(配置了切面)
因为updateAccount(int i)有参数,通配写法没有表现出来。
6.参数列表:
可以直接写数据类型
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
* *..*.*(int) : 此时只有一个方法配置了切面(增强了):因为只有一个方法有参数
类型使用通配符,表示任意类型,但是必须有参数 : 表明有参数(任意参数,但必须有参数)
* *..*.*(*) : 还是只有一个方法配置了切面(增强了):因为只有一个方法有参数
可以使用..表示有无参数都可,有参数可以是任意类型。
* *..*.*(..) : 三个方法都被增强了。
* 小结
但是在实际开发中,不要写成这个全通配的方式:
这样程序在执行时,所有所有的返回都满足这个通配写法。都会被配置切面,添加增强方法。
这不是我们想要的
建议的写法:切入点表达式的通常写法
切到业务层实现类下的所有方法:省略了修饰符/类/方法名
* com.itheima.service.impl.*.*(..)
4.4 四种通知类型
1. aop:before : 配置前置通知的类型
在切入点方法之前执行
2. aop:after-returning : 配置后置通知的类型----代表着切入点方法成功执行。
在切入点方法成功的执行之后才会执行后置通知
3. after-throwing : 配置异常通知的类型
切入点方法出现异常后,会执行异常通知
4. aop:after : 配置最终通知的类型
在finally()里面的通知,无论切入点方法是否正常之心,一定会执行。
所以最多执行三个:
后置通知和异常通知只有一个能执行成功。
5.再改进一下切面配置:抽取切入点表达式
pointcut="execution(* com.itheima.service.impl.*.*(..))"
配置切入点表达式:
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
配置好之后,上面4行的就不用在写pointcut属性了,引用此处的aop:pointcut标签即可
6.继续改进
此标签写在aop:aspect标签内部只能当前切面使用,如果有新的切面那么重新配。
它还可以写在aop:aspect标签外面,此时就变成了所有切面可用。
注意:
虽然可以放到aop:aspect标签外面,但是还要遵循头部的约束要求:出现在切面之前,
写在aop:aspect标签的前面,不然会报错。
4.4 环绕通知类型
1定义一个环绕通知方法
2.配置环绕通知
3.测试会调用方法业务层的方法
4.测试之后,出现一个问题
业务层中的方法没有打印语句,而环绕通知中的语句输出了。
当我们配置了环绕通知之后,切入点方法没有直行,而通知方法执行了。这显然
不是我们希望看到的,那到底是什么原因造成的呢?
我们分析一下这个环绕通知和上一个例子中的切入点有何不同:
最大的区别就是:
一个有明显的切入点方法的调用,一个没有。
4.5 代码方式实现--环绕通知类型
我们除了靠配置来实现切面(增强代码/通知和切入点方法的组合方式),也可以通过代
码的方式实现。
1.分析:
通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,
而此处的环绕通知没有。
2.解决:我们也要在环绕通知里调用切入点方法
Spring框架为我们提供了一个接口:ProceedingJoinPoint。
Spring框架为我们提供了一个接口:ProceedingJoinPoint。
该接口有一个方法proceed(),
此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,
在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
简单来说:拿去用就行了,其他的spring都给你弄好了。
3. 修改原来的环绕通知方法:aroundPrintLog()
* 传入参数 ProceedingJoinPoint pjp
* 调用其中的方法:pjp.proceed()
* 增强方法:通知
System.out.println("Logger类中的环绕通知aroundPrintLog()开始记录日志了");
写在pjp.proceed()之前就是前置
写在pjp.proceed()之后就是后置
写在catch里面就是异常
写在finally里面就是最终
最后这整个aroundPrintLog()就变成了环绕通知。
*注意:注意:此处异常必须写Throwable t ; Exception拦不住它
4. 小结:通知其实都是增强的代码的一部分。
我们之前讲的四种通知类型,都是通过配置的方式来指定增强的代码什么时候执行。
而现在我们是通过代码控制的方式来指定增强的代码何时执行,所以说spring中的环绕
通知,其实他有另外的一种解释:
它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
我们除了靠配置来实现切面(增强代码/通知和切入点方法的组合方式),也可以通过代
码的方式实现。
5. spring中基于XML和注解的AOP配置
1.改成IOC的注解:改成context的名称空间---bean.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"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
```
2.bean.xml的其他配置
<!--配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置springAOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
3. 也可以把一点XML都不写:写一个配置类 SpringConfiguration
4. 注解通知类
5.指定切入点(被增强的方法)表达式
6.注解 通知方法
注意:要传入切入点表达式
@Before("pt1()")
@AfterReturning("pt1()")
@AfterThrowing("pt1()")
@After("pt1()")
@Around("pt1()")
7.Spring基于注解的配置AOP时:
* 通知的执行调用是有问题的,所以在选择上要有个考量。
* 环绕通知是没有问题的:
因为是我们在代码中手动控制增强方法何时执行的方式,我们想让他们什么时候执行
就执行,明确定义了顺序。
* 显然注解非常简单,但是的执行调用是有问题的。
6 总结
重复的代码:
先抽取,再在方法执行时通过动态代理给它加进去,不改变源码增强了原有的方法。
最后才由Spring来实现AOP。