Spring之——AOP基础、回顾动态代理

什么是AOP

AOP:全称是 Aspect Oriented Programming 即:面向切面编程。

简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

AOP 的作用及优势

  • 作用:在程序运行期间,不修改源码对已有方法进行增强。
  • 优势:
    • 减少重复代码
    • 提高开发效率
    • 维护方便

AOP 的实现方式:使用动态代理技术

AOP的具体作用

发现问题

在我们进行数据库的增删改查中,业务层中单条SQL语句是没问题的,因为执行一条SQL语句会创建一个Connection控制事务,但如果是多条语句,就会创建是多个Connection,这就产生了一个问题:不符合事务的一致性,如果SQL语句执行过程中出现异常,那么数据就会出问题。

举个例子:银行转账
1、转账账户 - 转账金额
2、转账账户数据操作成功
3、收钱账户 + 转账金额

转账需要经过1和3这两个步骤才能实现,但如果步骤2位置出现异常,那么就会出现这么一个情况
转账账户的钱少了,但是收钱账户的钱却没增加

解决问题

既然已经发现问题的出现是因为事务的不一致,那么我们就可以想办法让同一个线程里面统一使用一个Connection来控制事务

解决方法:
1、创建一个Connection工具类,它可以绑定一个Connection在某个线程上,如果当前线程没有绑定Connection,那么从连接池中获取一个再绑定
2、创建一个事务管理类,它通过Connection工具类获取当前线程绑定的Connection对象,然后进行相应的事务管理

ConnectionUtil连接的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.itheima.utils;

import javax.sql.DataSource;
import java.sql.Connection;

/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {

private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

private DataSource dataSource;

public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

/**
* 获取当前线程上的连接
* @return
*/
public Connection getThreadConnection() {
try{
//1.先从ThreadLocal上获取
Connection conn = tl.get();
//2.判断当前线程上是否有连接
if (conn == null) {
//3.从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;
}catch (Exception e){
throw new RuntimeException(e);
}
}

/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}

TransactionManager事务控制类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.itheima.utils;

/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
public class TransactionManager {

private ConnectionUtils connectionUtils;

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();
}
}
}


DAO层实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.itheima.dao.impl;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import java.util.List;

/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {

private QueryRunner runner;
private ConnectionUtils connectionUtils;

public void setRunner(QueryRunner runner) {
this.runner = runner;
}

public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}

@Override
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);
}
}

@Override
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);
}
}

@Override
public void saveAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public void updateAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public void deleteAccount(Integer accountId) {
try{
runner.update(connectionUtils.getThreadConnection(),"delete from account where id=?",accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public Account findAccountByName(String accountName) {
try{
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
if(accounts == null || accounts.size() == 0){
return null;
}
if(accounts.size() > 1){
throw new RuntimeException("结果集不唯一,数据有问题");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}

Service业务层实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package com.itheima.service.impl;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager;

import java.util.List;

/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl_OLD implements IAccountService{

private IAccountDao accountDao;
private TransactionManager txManager;

public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}

public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}

@Override
public List<Account> findAllAccount() {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事务
txManager.commit();
//4.返回结果
return accounts;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}

}

@Override
public Account findAccountById(Integer accountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事务
txManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}

@Override
public void saveAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.saveAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}

}

@Override
public void updateAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.updateAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}

}

@Override
public void deleteAccount(Integer acccountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.deleteAccount(acccountId);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}

}

@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作

//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
accountDao.updateAccount(source);

int i=1/0;

//2.6更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();

}catch (Exception e){
//4.回滚操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.释放连接
txManager.release();
}


}
}

Spring IOC配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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">

<!--配置代理的service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

<!--配置beanfactory-->
<bean id="beanFactory" class="com.itheima.factory.BeanFactory">
<!-- 注入service -->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务管理器 -->
<property name="txManager" ref="txManager"></property>
</bean>

<!-- 配置Service -->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
</bean>

<!--配置Dao对象-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>

<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="user" value="root"></property>
<property name="password" value="1234"></property>
</bean>

<!-- 配置Connection的工具类 ConnectionUtils -->
<bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 配置事务管理器-->
<bean id="txManager" class="com.itheima.utils.TransactionManager">
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>

Maven坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>

<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

引申出新的问题

现在已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了一个新的问题:

业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了。

试想一下,如果我们此时提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码
况且这还只是一个业务层实现类,而实际的项目中这种业务层实现类可能有十几个甚至几十个。

这时,我们就可以使用动态代理技术来解决这个问题

回顾动态代理:动态代理的特点

字节码随用随创建,随用随加载。

它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。

装饰者模式就是静态代理的一种体现。

动态代理常用的有两种方式

  • 基于接口的动态代理
    • 提供者:JDK 官方的 Proxy 类。
    • 要求:被代理类最少实现一个接口。
  • 基于子类的动态代理
    • 提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
    • 要求:被代理类不能用 final 修饰的类(最终类)。

1、基于接口的动态代理

  • 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.itheima.proxy;

    /**
    * 对生产厂家要求的接口
    */
    public interface IProducer {

    /**
    * 销售
    * @param money
    */
    public void saleProduct(float money);

    /**
    * 售后
    * @param money
    */
    public void afterService(float money);
    }

  • 接口实现类(生产者)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.itheima.proxy;

    /**
    * 一个生产者
    */
    public class Producer implements IProducer{

    /**
    * 销售
    * @param money
    */
    public void saleProduct(float money){
    System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
    * 售后
    * @param money
    */
    public void afterService(float money){
    System.out.println("提供售后服务,并拿到钱:"+money);
    }
    }

  • 创建动态代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    package com.itheima.proxy;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    /**
    * 模拟一个消费者
    */
    public class Client {

    public static void main(String[] args) {
    final Producer producer = new Producer();

    /**
    * 动态代理:
    * 特点:字节码随用随创建,随用随加载
    * 作用:不修改源码的基础上对方法增强
    * 分类:
    * 基于接口的动态代理
    * 基于子类的动态代理
    * 基于接口的动态代理:
    * 涉及的类:Proxy
    * 提供者:JDK官方
    * 如何创建代理对象:
    * 使用Proxy类中的newProxyInstance方法
    * 创建代理对象的要求:
    * 被代理类最少实现一个接口,如果没有则不能使用
    * newProxyInstance方法的参数:
    * ClassLoader:类加载器
    * 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
    * Class[]:字节码数组
    * 它是用于让代理对象和被代理对象有相同方法。固定写法。
    * InvocationHandler:用于提供增强的代码
    * 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
    * 此接口的实现类都是谁用谁写。
    */
    IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
    producer.getClass().getInterfaces(),
    new InvocationHandler() {
    /**
    * 作用:执行被代理对象的任何接口方法都会经过该方法
    * 方法参数的含义
    * @param proxy 代理对象的引用
    * @param method 当前执行的方法
    * @param args 当前执行方法所需的参数
    * @return 和被代理对象方法有相同的返回值
    * @throws Throwable
    */
    @Override
    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;
    }
    });
    proxyProducer.saleProduct(10000f);
    }
    }

2、基于子类的动态代理

  • 生产者(被代理的类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.itheima.cglib;

    /**
    * 一个生产者
    */
    public class Producer {

    /**
    * 销售
    * @param money
    */
    public void saleProduct(float money){
    System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
    * 售后
    * @param money
    */
    public void afterService(float money){
    System.out.println("提供售后服务,并拿到钱:"+money);
    }
    }

  • 消费者(创建动态代理)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    package com.itheima.cglib;

    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;

    import java.lang.reflect.Method;

    /**
    * 模拟一个消费者
    */
    public class Client {

    public static void main(String[] args) {
    final Producer producer = new Producer();

    /**
    * 动态代理:
    * 特点:字节码随用随创建,随用随加载
    * 作用:不修改源码的基础上对方法增强
    * 分类:
    * 基于接口的动态代理
    * 基于子类的动态代理
    * 基于子类的动态代理:
    * 涉及的类:Enhancer
    * 提供者:第三方cglib库
    * 如何创建代理对象:
    * 使用Enhancer类中的create方法
    * 创建代理对象的要求:
    * 被代理类不能是最终类
    * create方法的参数:
    * Class:字节码
    * 它是用于指定被代理对象的字节码。
    *
    * Callback:用于提供增强的代码
    * 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
    * 此接口的实现类都是谁用谁写。
    * 我们一般写的都是该接口的子接口实现类:MethodInterceptor
    */
    Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
    /**
    * 执行北地阿里对象的任何方法都会经过该方法
    * @param proxy
    * @param method
    * @param args
    * 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
    * @param methodProxy :当前执行方法的代理对象
    * @return
    * @throws Throwable
    */
    @Override
    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);
    }
    }

使用动态代理技术解决刚刚发现的新问题:业务层事务管理耦合

在上面代码的基础上修改

  • 创建Service的代理对象的工厂(通过动态代理创建Service这个生产者的增强对象)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    package com.itheima.factory;

    import com.itheima.service.IAccountService;
    import com.itheima.utils.TransactionManager;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    /**
    * 用于创建Service的代理对象的工厂
    */
    public class BeanFactory {

    private IAccountService accountService;

    private TransactionManager txManager;

    public void setTxManager(TransactionManager txManager) {
    this.txManager = txManager;
    }


    public final void setAccountService(IAccountService accountService) {
    this.accountService = accountService;
    }

    /**
    * 获取Service代理对象
    * @return
    */
    public IAccountService getAccountService() {
    return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
    accountService.getClass().getInterfaces(),
    new InvocationHandler() {
    /**
    * 添加事务的支持
    *
    * @param proxy
    * @param method
    * @param args
    * @return
    * @throws Throwable
    */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    if("test".equals(method.getName())){
    return method.invoke(accountService,args);
    }

    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();
    }
    }
    });

    }
    }

  • 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.itheima.test;

    import com.itheima.service.IAccountService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

    /**
    * 使用Junit单元测试:测试我们的配置
    */
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:bean.xml")
    public class AccountServiceTest {

    @Autowired
    @Qualifier("proxyAccountService")
    private IAccountService as;

    @Test
    public void testTransfer(){
    as.transfer("aaa","bbb",100f);
    }

    }

Spring中的AOP

Spring中的AOP其实就是通过配置方式,实现上面的内容

AOP相关术语

在开发中可能用不到什么,但是资料上会出现这些术语,为了以后的学习,需要慢慢消化它

  • Joinpoint(连接点):

    所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。

  • Pointcut(切入点):

    所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。

  • Advice(通知/增强):

    所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。

    通知的类型:

    • 前置通知
    • 后置通知
    • 异常通知
    • 最终通知
    • 环绕通知。
  • Introduction(引介):

    引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。

  • Target(目标对象):

    代理的目标对象。

  • Weaving(织入):

    是指把增强应用到目标对象来创建新的代理对象的过程。
    spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。

  • Proxy(代理):

    一个类被 AOP 织入增强后,就产生一个结果代理类。

  • Aspect(切面):

    是切入点和通知(引介)的结合。

学习 spring 中的 AOP 要明确的事

开发阶段(我们做的)

  • 1、编写核心业务代码(开发主线)
  • 2、把公用代码抽取出来,制作成通知。(开发阶段最后再做)
  • 3、在配置文件中,声明切入点与通知间的关系,即切面。

运行阶段(Spring 框架完成的)

Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对
象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

关于代理的选择

在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

实现XML的AOP配置

1、导入Maven坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>

2、创建创建Spring配置文件,注意约束的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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">

<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

<!--spring中基于XML的AOP配置步骤
1、把通知Bean也交给spring来管理
2、使用aop:config标签表明开始AOP的配置
3、使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
4、在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)

实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
-->

<!-- 配置Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>

<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 可以配置通用表达式,在配置通知时,使用pointcut-ref="pt1"即可-->
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/>

<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>

</beans>

logger日志对象(通用的通知对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itheima.utils;

/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {

/**
* 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的pringLog方法开始记录日志了。。。");
}
}

讲解XML的AOP配置

aop:config:

  • 作用:用于声明开始 aop 的配置
    1
    2
    3
    <aop:config>
    <!-- 配置的代码都写在此处 -->
    </aop:config>

aop:aspect:

  • 作用:用于配置切面。
  • 属性:
    • id:给切面提供一个唯一标识。
    • ref:引用配置好的通知类 bean 的 id。
  • 1
    2
    3
    <aop:aspect id="txAdvice" ref="txManager">
    <!--配置通知的类型要写在此处-->
    </aop:aspect>

aop:pointcut:

  • 作用:用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
  • 属性:
    • expression:用于定义切入点表达式。
    • id:用于给切入点表达式提供一个唯一标识
  • 1
    <aop:pointcut expression="表达式" id="pt1"/>

aop:before

  • 作用:用于配置前置通知。指定增强的方法在切入点方法之前执行
  • 属性:
    • method:用于指定通知类中的增强方法名称
    • ponitcut-ref:用于指定切入点的表达式的引用
    • poinitcut:用于指定切入点表达式
  • 执行时间点:切入点方法执行之前执行
    1
    <aop:before method="beginTransaction" pointcut-ref="pt1"/>

aop:after-returning:

  • 作用:用于配置后置通知
  • 属性:
    • method:指定通知中方法的名称。
    • pointct:定义切入点表达式
    • pointcut-ref:指定切入点表达式的引用
  • 执行时间点:切入点方法正常执行之后。它和异常通知只能有一个执行
    1
    <aop:after-returning method="commit" pointcut-ref="pt1"/>

aop:after-throwing

  • 作用:用于配置异常通知
  • 属性:
    • method:指定通知中方法的名称。
    • pointct:定义切入点表达式
    • pointcut-ref:指定切入点表达式的引用
  • 执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个
    1
    <aop:after-throwing method="rollback" pointcut-ref="pt1"/>

aop:after

  • 作用:用于配置最终通知
  • 属性:
    • method:指定通知中方法的名称。
    • pointct:定义切入点表达式
    • pointcut-ref:指定切入点表达式的引用
  • 执行时间点:无论切入点方法执行时是否有异常,它都会在其后面执行。
    1
    <aop:after method="release" pointcut-ref="pt1"/>

切入点表达式说明

  • execution:匹配方法的执行(常用)
  • execution(表达式)
    • 表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
  • 写法说明:

    完整写法
    public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

    访问修饰符可以省略
    void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

    返回值可以使用*号,表示任意返回值
    * com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

    包名可以使用*号,表示任意包,但是有几级包,需要写几个*
    * *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)

    使用..来表示当前包,及其子包
    * com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)

    类名可以使用*号,表示任意类
    * com..*.saveAccount(com.itheima.domain.Account)

    方法名可以使用*号,表示任意方法
    * com..*.*( com.itheima.domain.Account)

    参数列表可以使用*,表示参数可以是任意数据类型,但是必须有参数
    * com..*.*(*)

    参数列表可以使用..表示有无参数均可,有参数可以是任意类型
    * com..*.*(..)

    全通配方式:
    * *..*.*(..)

    注:通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
    execution(* com.itheima.service.impl.*.*(..))

环绕通知

  • 配置方式:

    1
    2
    3
    4
    5
    6
    7
    <aop:config>
    <aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/>
    <aop:aspect id="txAdvice" ref="txManager">
    <!-- 配置环绕通知 -->
    <aop:around method="transactionAround" pointcut-ref="pt1"/>
    </aop:aspect>
    </aop:config>
  • aop:around:

    • 作用:用于配置环绕通知
    • 属性:
      • method:指定通知中方法的名称。
      • pointct:定义切入点表达式
      • pointcut-ref:指定切入点表达式的引用
    • 说明:它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
    • 注意:通常情况下,环绕通知都是独立使用的
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
    * 环绕通知
    * @param pjp
    * spring 框架为我们提供了一个接口:ProceedingJoinPoint,它可以作为环绕通知的方法参数。
    * 在环绕通知执行时,spring 框架会为我们提供该接口的实现类对象,我们直接使用就行。
    * @return
    */
    public Object transactionAround(ProceedingJoinPoint pjp) {
    //定义返回值
    Object rtValue = null;
    try {

    //获取方法执行所需的参数
    Object[] args = pjp.getArgs();
    //前置通知:开启事务
    beginTransaction();
    //执行方法
    rtValue = pjp.proceed(args);
    //后置通知:提交事务
    commit();

    }catch(Throwable e) {

    //异常通知:回滚事务
    rollback();
    e.printStackTrace();

    }finally {

    //最终通知:释放资源
    release();

    }
    return rtValue;
    }

基于注解的AOP配置

Spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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">

<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>

<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

logger通知对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.itheima.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
@Component("logger")//Spring IOC 创建logger对象
@Aspect//表示当前类是一个切面类
public class Logger {

//配置一个通用切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}

/**
* 前置通知
*/
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}

/**
* 后置通知
*/
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}

/**
* 最终通知
*/
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}

/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
*
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*/
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数

System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法,相当于动态代理中的invork)

System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
}

Service业务层实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.itheima.service.impl;

import com.itheima.service.IAccountService;
import org.springframework.stereotype.Service;

/**
* 账户的业务层实现类
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService{

@Override
public void saveAccount() {
System.out.println("执行了保存");
}

@Override
public void updateAccount(int i) {
System.out.println("执行了更新"+i);

}

@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}

不使用 XML 的配置方式

1
2
3
4
5
@Configuration//用于IOC
@ComponentScan(basePackages="com.itheima")//用于IOC
@EnableAspectJAutoProxy//用于AOP
public class SpringConfiguration {
}

ps:使用注解的方式配置 前置通知 后置通知。。。等 会出现一个问题,就是最终通知会在 异常通知 与 后置通知 之前执行,不过注解的环绕通知不会出现这个情况,所以如果要用注解配置AOP,那么最好用环绕通知。